package bsh;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bsh.asm.acessor.ObjectAccessor;
import bsh.exception.CompilationFailedException;
import bsh.meta.FieldDescriptor;
import bsh.meta.JavaClassWrapper;
import bsh.meta.Scope;
import bsh.meta.TypeWrapper;
import bsh.meta.UnfinishedInnerType;

/**
 * Node compiler for a class or interface declaration. It creates a public class (that is not declared as an inner class). Multiple
 * invocations of {@link #compile(MethodVisitor, CompilationState)} do not trigger multiple class creations. Other places that need to use
 * the (yet) unparsed class, may use the {@link #getUnfinishedType(Scope)} as a surrogate.
 * 
 * @author takeshi
 */
public class ClassDeclarationNodeCompiler implements NodeCompiler {

    private final BSHClassDeclaration node;

    private final CompilationUnit unit;

    private BSHBlock classBody;

    private TypeWrapper superType = JavaClassWrapper.OBJECT_WRAPPER;

    private TypeWrapper[] interfaces;

    private UnfinishedInnerType innerType;

    private boolean initialized;

    private final String className;

    /**
     * Constructor
     * 
     * @param unit the {@link CompilationUnit}
     * @param node the {@link BSHClassDeclaration}
     */
    public ClassDeclarationNodeCompiler(CompilationUnit unit, BSHClassDeclaration node) {
        this.node = node;
        this.unit = unit;
        this.className = this.unit.thisType().internalName() + "$" + this.unit.thisType().newDynamicInvocationId() + "$" + this.node.name;
    }

    /**
     * {@inheritDoc}
     */
    public void compile(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        if (initialized) {
            return;
        }
        Scope scope = state.currentScope();
        compileClassDeclaration(scope);
    }

    /**
     * Compiles the class declaration
     * 
     * @param declaringNode the declaring node
     * @return {@link UnfinishedInnerType}
     * @throws CompilationFailedException e
     */
    public UnfinishedInnerType compileClassDeclaration(Scope declaringNode) throws CompilationFailedException {
        if (initialized) {
            return innerType;
        }
        int i = 0;
        if (node.extend) {
            Node child = node.getChild(i++);
            this.superType = resolveAsType(child, declaringNode);
        }
        this.interfaces = new TypeWrapper[this.node.numInterfaces];
        for (int j = 0; j < this.node.numInterfaces; j++) {
            interfaces[j] = resolveAsType(node.getChild(i++), declaringNode);
        }
        if (i < node.jjtGetNumChildren()) {
            this.classBody = (BSHBlock) node.getChild(i);
        }

        if (this.node.isInterface) {
            this.innerType = this.compileInterface(declaringNode);
        } else {
            this.innerType = this.compileInstanceType(declaringNode);
        }
        this.initialized = true;
        return innerType;
    }

    /**
     * Resulves a node as type. The node must be an {@link BSHAmbiguousName}
     * 
     * @param child the node
     * @param scope {@link Scope}
     * @return {@link TypeWrapper}
     */
    private TypeWrapper resolveAsType(Node child, Scope scope) {
        return scope.resolveType(((BSHAmbiguousName) child).text);
    }

    /**
     * Compiles an interface definition
     * 
     * @param declaringScope the declaring scope
     * @return {@link UnfinishedInnerType} that represents this interface
     * @throws CompilationFailedException e
     */
    private UnfinishedInnerType compileInterface(Scope declaringScope) throws CompilationFailedException {
        CompilationUnit innerUnit =
                this.unit.createInnerType(this.className, this.superType, Collections.<TypeWrapper> emptyList(), declaringScope,
                        Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT | Opcodes.ACC_INTERFACE | Opcodes.ACC_STATIC, this.node.name);
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        String[] interfaceNames = new String[]{ /* this.superType.internalName() */}; // as of now, only 1 interface is allowed
        writer.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT | Opcodes.ACC_INTERFACE, this.className, null,
                JavaClassWrapper.OBJECT_WRAPPER.internalName(), interfaceNames);
        if (this.unit.thisType().getSourceCodeFileName() != null) {
            writer.visitSource(this.unit.thisType().getSourceCodeFileName(), null);
        }
        List<BytecodeGenerator> staticInitializer = createInterfaceDefinition(this.classBody, innerUnit, writer, declaringScope);
        if (!staticInitializer.isEmpty()) {
            MethodVisitor visitor = writer.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
            visitor.visitCode();
            CompilationState state = new CompilationState(innerUnit);
            for (BytecodeGenerator bytecodeGenerator : staticInitializer) {
                bytecodeGenerator.generateBytecode(visitor, innerUnit, state);
            }
            visitor.visitMaxs(0, 0);
            visitor.visitEnd();
        }
        writer.visitEnd();
        this.unit.addInnerClassDefinition(writer.toByteArray(), (UnfinishedInnerType) innerUnit.thisType());
        declaringScope.addTypeAlias(this.node.name, innerUnit.thisType());
        return (UnfinishedInnerType) innerUnit.thisType();
    }

    /**
     * Creates an interface definition. This methods only reads variables (as public static final ones), method signatures (it ignores
     * method bodies) and inner classes definitins
     * 
     * @param classBody the class body
     * @param compilationUnit the {@link CompilationUnit} that contains this interface definition
     * @param writer the {@link ClassWriter} to write this interface definition to
     * @param scope the declared scope
     * @return {@link List} of {@link BytecodeGenerator} to be put into the static initializer of the interface
     * @throws CompilationFailedException e
     */
    public static List<BytecodeGenerator> createInterfaceDefinition(SimpleNode classBody, CompilationUnit compilationUnit,
            ClassWriter writer, Scope scope) throws CompilationFailedException {
        if (classBody == null) {
            return Collections.emptyList();
        }
        List<BytecodeGenerator> staticInitializer = new ArrayList<BytecodeGenerator>();
        writer.toString();
        for (int i = 0; i < classBody.jjtGetNumChildren(); i++) {
            SimpleNode currentNode = classBody.getChild(i);
            if (currentNode instanceof BSHTypedVariableDeclaration) {
                BSHTypedVariableDeclaration dec = (BSHTypedVariableDeclaration) currentNode;
                TypedVariableDeclarationCompiler compiler = new TypedVariableDeclarationCompiler(dec, compilationUnit);
                final TypeWrapper varType = compiler.variableType(scope);
                for (int j = 0; j < dec.jjtGetNumChildren(); j++) {
                    if (!(dec.getChild(j) instanceof BSHVariableDeclarator)) {
                        continue;
                    }
                    final BSHVariableDeclarator declarator = (BSHVariableDeclarator) dec.getChild(j);
                    FieldDescriptor field =
                            compilationUnit.addStaticField(declarator.name, varType, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL);
                    writer.visitField(field.asmModifiers(), field.fieldName(), field.fieldType().typeDescription(), null, null).visitEnd();

                    if (declarator.jjtGetNumChildren() == 1) {
                        staticInitializer.add(new BytecodeGenerator() {

                            public void generateBytecode(MethodVisitor visitor, CompilationUnit theUnit, CompilationState state)
                                    throws CompilationFailedException {
                                int size = state.getStackSize();
                                ObjectAccessor acc = theUnit.getFieldAcessor(declarator.name);
                                acc.preStore(visitor, state);
                                theUnit.compileNode(declarator.getChild(0), visitor, state);
                                theUnit.getCompilerManager().coerceStackTopTo(visitor, varType, state);
                                acc.storeObject(visitor, state);
                                state.assertStackSize(size);
                            }
                        });
                    }
                }

            } else if (currentNode instanceof BSHBlock) {
                staticInitializer.add(createInitializerCode(currentNode));
            } else if (currentNode instanceof BSHMethodDeclaration) {
                BSHMethodDeclaration methodDeclaration = (BSHMethodDeclaration) currentNode;
                final MethodDeclarationParser methodSignatureParser =
                        new MethodDeclarationParser(methodDeclaration, compilationUnit, compilationUnit.getGlobalScope());
                if (methodSignatureParser.isConstructor()) { // its a constructor
                    throw new CompilationFailedException(currentNode.getLineNumber(), "Cannot define a constructor inside an interface");
                } else {
                    writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, methodDeclaration.name,
                            methodSignatureParser.getMethodDescriptor().toDescription(), null, null).visitEnd();
                }
            } else if (currentNode instanceof BSHClassDeclaration) {
                ClassDeclarationNodeCompiler compiler = (ClassDeclarationNodeCompiler) compilationUnit.getNodeCompiler(currentNode);
                compiler.compile(null, new CompilationState(compilationUnit));
            } else {
                throw new CompilationFailedException(currentNode.getLineNumber(), "Cannot use node type: " + currentNode.getClass()
                        + " inside an interface");
            }
        }
        return staticInitializer;
    }

    /**
     * Parses a node into a {@link BytecodeGenerator}. It only delays the parsing to the compiler to the moment
     * {@link BytecodeGenerator#generateBytecode(MethodVisitor, CompilationUnit, CompilationState)} is called.
     * 
     * @param currentNode the node
     * @return {@link BytecodeGenerator}
     */
    public static BytecodeGenerator createInitializerCode(final SimpleNode currentNode) {
        return new BytecodeGenerator() {

            public void generateBytecode(MethodVisitor visitor, CompilationUnit unit2, CompilationState state)
                    throws CompilationFailedException {
                unit2.compileNode(currentNode, visitor, state);
            }
        };
    }

    /**
     * Compiles an instance type - regular inner classes
     * 
     * @param declaringScope the declaring scope
     * @return the {@link UnfinishedInnerType}
     * @throws CompilationFailedException e
     */
    private UnfinishedInnerType compileInstanceType(Scope declaringScope) throws CompilationFailedException {
        return this.unit.getCompilerManager().compileInnerClass(this.className, this.node.name, this.unit, this.classBody, declaringScope,
                superType, interfaces, readClassModifiers());
    }

    /**
     * Reads the class modifiers from the declaring node. At least it uses the public modifier.
     * 
     * @return int
     */
    private int readClassModifiers() {
        if (this.node.modifiers == null) {
            return Opcodes.ACC_PUBLIC;
        }
        int mod = Opcodes.ACC_PUBLIC;
        if (this.node.modifiers.hasModifier("static")) {
            mod |= Opcodes.ACC_STATIC;
        }
        if (this.node.modifiers.hasModifier("abstract")) {
            mod |= Opcodes.ACC_ABSTRACT;
        }
        if (this.node.modifiers.hasModifier("synchronized")) {
            mod |= Opcodes.ACC_SYNCHRONIZED;
        }
        if (this.node.modifiers.hasModifier("final")) {
            mod |= Opcodes.ACC_FINAL;
        }
        return mod;
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper nodeResultType(Scope scope) throws CompilationFailedException {
        return JavaClassWrapper.VOID_WRAPPER;
    }

    /**
     * @return the innerType
     */
    public UnfinishedInnerType getInnerType() {
        return innerType;
    }

    /**
     * @param innerType the innerType to set
     */
    public void setInnerType(UnfinishedInnerType innerType) {
        this.innerType = innerType;
    }

    /**
     * Used to eagerly get the {@link UnfinishedInnerType} that represents the inner type generated by this compiler.
     * 
     * @param scope {@link Scope} the declaring scope or the global scope if the former is not available
     * @return {@link UnfinishedInnerType}
     */
    public UnfinishedInnerType getUnfinishedType(Scope scope) {
        if (this.innerType != null) {
            return innerType;
        }
        this.innerType =
                new LazyInitializingUnfinishedInnerType(scope, this.unit.thisType().getSourceCodeFileName(), this.className,
                        this.node.name, this, readClassModifiers());
        return this.innerType;
    }

    /**
     * @return the declaring compilation unit
     */
    public CompilationUnit getUnit() {
        return unit;
    }

}
